package ph.com.gs3.formalistics.service.managers;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import ph.com.gs3.formalistics.global.utilities.logging.FLLogger;
import ph.com.gs3.formalistics.model.communicators.DocumentCommunicatorModule;
import ph.com.gs3.formalistics.model.communicators.HttpCommunicatorModule.CommunicatorException;
import ph.com.gs3.formalistics.model.dao.DocumentsDAO;
import ph.com.gs3.formalistics.model.dao.FormsDAO;
import ph.com.gs3.formalistics.model.dao.OutgoingDocumentsDAO;
import ph.com.gs3.formalistics.model.dao.OutgoingStarMarksDAO;
import ph.com.gs3.formalistics.model.valueobjects.application.APIResponse;
import ph.com.gs3.formalistics.model.valueobjects.application.APIResponse.APIInvalidResponseException;
import ph.com.gs3.formalistics.model.valueobjects.application.APIResponse.APIServerErrorException;
import ph.com.gs3.formalistics.model.valueobjects.application.APIResponse.APIStatus;
import ph.com.gs3.formalistics.model.valueobjects.business.document.Document;
import ph.com.gs3.formalistics.model.valueobjects.business.document.DocumentStub;
import ph.com.gs3.formalistics.model.valueobjects.business.document.OutgoingDocument;
import ph.com.gs3.formalistics.model.valueobjects.business.document.OutgoingStarMark;
import ph.com.gs3.formalistics.model.valueobjects.business.document.Document.DocumentStar;
import ph.com.gs3.formalistics.model.valueobjects.business.form.FormStub;
import ph.com.gs3.formalistics.view.valueobjects.parsers.DateUtilities;
import android.database.SQLException;

public class DocumentManager {

	public static final String TAG = DocumentManager.class.getSimpleName();

	private FormsDAO formsDAO;
	private DocumentsDAO documentsDAO;
	private OutgoingDocumentsDAO outgoingDocumentsDAO;
	private OutgoingStarMarksDAO outgoingStarMarksDAO;
	private DocumentCommunicatorModule documentCommModule;

	public DocumentManager(FormsDAO formsDAO, DocumentsDAO documentsDAO,
	        OutgoingDocumentsDAO outgoingDocumentsDAO, OutgoingStarMarksDAO outgoingStarMarksDAO,
	        DocumentCommunicatorModule documentCommModule) {

		this.formsDAO = formsDAO;
		this.documentsDAO = documentsDAO;
		this.outgoingDocumentsDAO = outgoingDocumentsDAO;
		this.outgoingStarMarksDAO = outgoingStarMarksDAO;
		this.documentCommModule = documentCommModule;

	}

	// =================================================================================
	// {{ Service Methods (Incoming Data From server)

	/**
	 * Returns the all documents' data from the server that were updated starting from the
	 * "lastDocumentsUpdate" specified.
	 * 
	 * WARNING! This will update the last_documents_update_search_date column of the form
	 * searched. Only call this function if you will update the documents.
	 * 
	 * @param formStubs
	 * @param lastDocumentsUpdate
	 * @return
	 */
	public GetDocumentUpdatesResult getDocumentUpdates(List<FormStub> formStubs) {

		GetDocumentUpdatesResult result = new GetDocumentUpdatesResult();

		result.documentsToUpdate = new ArrayList<DocumentStub>();

		// String lastDocumentsUpdate = configDAO.getLastDocumentsUpdate();
		String server = documentCommModule.getServer();

		int formWithDocumentUpdatesCount = 0;

		for (FormStub formStub : formStubs) {

			APIResponse downloadableDocumentsAPIResponse;

			// get the updates from web
			try {
				downloadableDocumentsAPIResponse = getDocumentUpdatesFromServer(formStub, server,
				        formStub.lastDocumentSearchUpdateDate);
			} catch (CommunicatorException | APIInvalidResponseException e) {
				FLLogger.w(TAG, "Download update failed: " + e.getMessage());
				result.exceptionList.add(e);
				continue;
			}

			// using the updates, check if the transaction is successful, if it is,
			// convert the results to a list of document stubs
			if (downloadableDocumentsAPIResponse.getStatus() == APIStatus.SUCCESS) {
				
				FLLogger.d(TAG, downloadableDocumentsAPIResponse.getResults());
				
				try {
					ArrayList<DocumentStub> currentFormDocumentStubs = (ArrayList<DocumentStub>) getDocumentsToUpdateFromAPIResponse(
					        downloadableDocumentsAPIResponse, formStub);

					result.documentsToUpdate.addAll(currentFormDocumentStubs);
					if (currentFormDocumentStubs.size() > 0) {
						formWithDocumentUpdatesCount++;
					}

					// update the last search date in the database
					formsDAO.open();
					formsDAO.setDocumentsLastUpdateDate(server, formStub.webId,
					        downloadableDocumentsAPIResponse.getServerDate());
					formsDAO.close();

				} catch (JSONException e) {
					APIInvalidResponseException convertedException = new APIInvalidResponseException(
					        "Server did not give a valid JSON array response while getting document update from form "
					                + formStub.name + ". The response was: "
					                + downloadableDocumentsAPIResponse.getResults(), e);
					result.exceptionList.add(convertedException);
					continue;
				}

			} else {

				// if the transaction is not successful, add the exception to the result's
				// exception list

				FLLogger.d(TAG,
				        "Server error: " + downloadableDocumentsAPIResponse.getErrorMessage());
				APIServerErrorException serverErrorException = new APIServerErrorException(
				        downloadableDocumentsAPIResponse.getErrorMessage());
				result.exceptionList.add(serverErrorException);
				continue;

			}

		}

		if (formWithDocumentUpdatesCount > 1) {
			// the documents belong to 2 or more forms, sort them first by their date
			Collections.sort(result.documentsToUpdate);
			FLLogger.d(TAG,
			        "There are 2 or more forms with documents that need update, the document update stubs are sorted.");
		}

		return result;

	}

	public Document updateOrSaveDocument(DocumentStub stub, String currentUserId)
	        throws DocumentDownloadFailedException, DocumentUpdateFailedException {

		String server = documentCommModule.getServer();

		APIResponse serverResponse = null;

		try {
			serverResponse = documentCommModule.getDocument(stub.formWebId, stub.webId);
		} catch (CommunicatorException | APIInvalidResponseException e) {
			throw new DocumentDownloadFailedException(e);
		}

		if (serverResponse.getStatus() == APIStatus.SUCCESS) {
			Document updateDocumentFromWeb;

			try {
				JSONObject responseJSON = new JSONObject(serverResponse.getResults());
				JSONObject rawDocumentJSON = new JSONObject(responseJSON.getString("request"));

				updateDocumentFromWeb = Document.createDocumentFromJSON(rawDocumentJSON, server,
				        stub.formWebId);

			} catch (JSONException e) {

				e.printStackTrace();
				FLLogger.d(TAG,
				        "Raw document response that caused error: " + serverResponse.getResults());
				throw new DocumentDownloadFailedException("Failed to interpret server response", e);

			}

			// update the document in the database
			documentsDAO.open();
			try {
				updateDocumentFromWeb.setOwnerId(currentUserId);
				Document updatedDocument = documentsDAO.saveOrUpdateDocument(updateDocumentFromWeb);
				// document is successfully downloaded and updated/saved
				return updatedDocument;
			} catch (JSONException | SQLException e) {
				throw new DocumentUpdateFailedException("Failed to save downloaded document", e);
			} finally {
				documentsDAO.close();
			}

		} else {
			throw new DocumentDownloadFailedException(serverResponse.getErrorMessage());
		}

	}

	// }}

	// =================================================================================
	// {{ Service Methods (Outgoing/Database)

	public void addDocumentToOutgoingDocuments(Document document, String formName, String action,
	        String issuedByUserId) throws DocumentSaveFailedException {

		try {
			outgoingDocumentsDAO.open();

			if ("0".equals(document.getWebId())) {
				// this is a new document, add the server where it belongs to
				String server = documentCommModule.getServer();
				document.setServer(server);
			}

			OutgoingDocument outgoingDocumentAction = OutgoingDocument.createFromDocument(document,
			        formName, action, issuedByUserId);

			// throws SQLException
			outgoingDocumentsDAO.saveOutgoingDocument(outgoingDocumentAction);

		} catch (SQLException e) {
			throw new DocumentSaveFailedException(e);
		} finally {
			outgoingDocumentsDAO.close();
		}

	}

	public void submitDocumentAction(OutgoingDocument outgoingDocument)
	        throws DocumentActionSubmitFailedException {

		String formWebId = outgoingDocument.getDocumentFormWebId();
		String documentWebId = outgoingDocument.getDocumentWebId();
		JSONObject requestData = outgoingDocument.getFieldValues();
		String action = outgoingDocument.getAction();

		APIResponse response;
		try {
			response = documentCommModule.submitDocumentAction(formWebId, documentWebId,
			        requestData, action);
			FLLogger.d(TAG, "results: " + response.getResults());

		} catch (CommunicatorException e) {
			throw new DocumentActionSubmitFailedException(e);
		} catch (APIInvalidResponseException e) {
			FLLogger.d(TAG, "Invalid response: " + e.getRawResponseString());
			throw new DocumentActionSubmitFailedException(e);
		}

		// TODO: Update the document here and clear the pending action for it

	}

	/**
	 * @deprecated Use markDocumentStar(String formWebId, String documentWebId, String
	 *             documentDbId, DocumentStar starMark) instead
	 */
	public void markDocumentStar(String formWebId, String documentWebId, DocumentStar star)
	        throws CommunicatorException, APIInvalidResponseException, APIServerErrorException {

		if (star == DocumentStar.STARRED) {
			FLLogger.d(TAG, "marking document " + documentWebId + " of form " + formWebId
			        + " as starred");
		} else {
			FLLogger.d(TAG, "marking document " + documentWebId + " of form " + formWebId
			        + " as unstarred");
		}

		String server = documentCommModule.getServer();

		APIResponse response = null;

		// mark document star in the server
		// throws CommunicatorException, APIInvalidResponseException
		response = documentCommModule.markDocumentStar(formWebId, documentWebId, star);

		if (response != null && response.getStatus() == APIStatus.SUCCESS) {
			// mark document star here in the device

			documentsDAO.open();
			documentsDAO.markDocumentStar(formWebId, documentWebId, server, star);
			documentsDAO.close();

		} else {
			throw new APIServerErrorException(response.getErrorMessage());
		}

		FLLogger.d(TAG, "done");

	}

	public void saveDocumentStarAsOutgoing(String formWebId, String documentWebId,
	        String documentDbId, String currentUserDbId, DocumentStar starMark) {

		String currentDateString = DateUtilities.getServerFormattedCurrentDate();
		String server = documentCommModule.getServer();

		// create and add a mark to the outgoing star marks table

		OutgoingStarMark outgoingStarMark = new OutgoingStarMark();

		outgoingStarMark.setDocumentId(documentDbId);
		outgoingStarMark.setStarMark(starMark);
		outgoingStarMark.setIssuedByUserDbId(currentUserDbId);
		outgoingStarMark.setDateIssued(currentDateString);

		outgoingStarMarksDAO.open();
		outgoingStarMarksDAO.saveOutgoingStarMark(outgoingStarMark);
		outgoingStarMarksDAO.close();

		// save the mark to the document

		documentsDAO.open();
		documentsDAO.markDocumentStar(formWebId, documentWebId, server, starMark);
		documentsDAO.close();

	}

	public void markDocumentStar(OutgoingStarMark outgoingStarMark)
	        throws DocumentActionSubmitFailedException {

		Document document = null;

		// check first if the document mark still needs to be updated
		documentsDAO.open();
		try {
			document = documentsDAO.getDocument(outgoingStarMark.getDocumentId());
			// TODO: check for a way to ignore star marks that are not needed anymore. The
			// condition below does not work.
			if (document.isStarred() == (outgoingStarMark.getStarMark() == DocumentStar.STARRED)) {
				// the star mark and the pending star mark is the same as the server's
				// update. there is no need to submit the star mark again.
				// Note: this can happen if the star mark is changed from starred then
				// unstarred again (and vice versa) while offline mode, this means that no
				// change is needed since the document's star mark is back to its original
				// state

				FLLogger.d(TAG,
				        "Ignored star mark submit request. It is not needed anymore for this document.");

				// return;
			}
		} catch (JSONException e) {
			// this should not happen
			e.printStackTrace();
		} finally {
			documentsDAO.close();
		}

		// mark document star in the server
		APIResponse response = null;

		String formWebId = document.getFormWebId();
		String documentWebId = document.getWebId();
		String documentDbId = document.getDbId();

		// throws CommunicatorException, APIInvalidResponseException
		try {
			response = documentCommModule.markDocumentStar(formWebId, documentWebId,
			        outgoingStarMark.getStarMark());
		} catch (CommunicatorException | APIInvalidResponseException e) {
			throw new DocumentActionSubmitFailedException(e);
		}

		if (response != null && response.getStatus() == APIStatus.SUCCESS) {
			// String markString = outgoingStarMark.getStarMark() == DocumentStar.STARRED
			// ? "Starred"
			// : "Unstarred";
			// FLLogger.d(TAG, markString + " document " +
			// outgoingStarMark.getDocumentId());

			// remove the outgoing star mark from the device's local database
			outgoingStarMarksDAO.open();
			outgoingStarMarksDAO.removeOutgoingStarMarkByDocumentId(documentDbId);
			outgoingStarMarksDAO.close();

		} else {
			FLLogger.d(TAG, response.getError());
			throw new DocumentActionSubmitFailedException(response.getErrorMessage());
		}

		FLLogger.d(TAG, "done");

	}

	// }}

	// =================================================================================
	// {{ Private Functional Methods

	private APIResponse getDocumentUpdatesFromServer(FormStub formStub, String server,
	        String lastDocumentsUpdate) throws CommunicatorException, APIInvalidResponseException {

		APIResponse downloadableFromsAPIResponse;

		documentsDAO.open();
		long availableDocumentCount = documentsDAO
		        .getDocumentsCountFromForm(server, formStub.webId);
		documentsDAO.close();

		if (availableDocumentCount > 0) {
			// throws CommunicatorException, APIInvalidResponseExceptio
			downloadableFromsAPIResponse = documentCommModule.getDocumentStubsToUpdate(
			        formStub.webId, lastDocumentsUpdate);
		} else {
			// if there are no available documents under this form, do not add a
			// last updated limit so all forms are automatically downloaded

			// throws CommunicatorException, APIInvalidResponseExceptio
			downloadableFromsAPIResponse = documentCommModule.getDocumentStubsToUpdate(
			        formStub.webId, null);
		}

		return downloadableFromsAPIResponse;

	}

	private List<DocumentStub> getDocumentsToUpdateFromAPIResponse(APIResponse response,
	        FormStub formStub) throws JSONException {

		if (response.getStatus() != APIStatus.SUCCESS) {
			throw new IllegalStateException(
			        "Tried to use getDocumentsToUpdateFromAPIResponse with a non successful API transaction response");
		}

		ArrayList<DocumentStub> currentFormDocumentStubs = new ArrayList<DocumentStub>();

		// throws JSONException
		JSONArray results = new JSONArray(response.getResults());

		// convert all the results to document stubs
		int resultingDocumentCount = results.length();
		for (int i = 0; i < resultingDocumentCount; i++) {
			// throws JSONException
			JSONObject documentStubJSON = results.getJSONObject(i);

			DocumentStub stub = new DocumentStub();

			stub.formWebId = formStub.webId;
			stub.webId = documentStubJSON.getString("ID");
			stub.dateUpdated = documentStubJSON.getString("DateUpdated");

			currentFormDocumentStubs.add(stub);

		}

		return currentFormDocumentStubs;

	}

	// }}

	// =================================================================================
	// {{ Private/Result Classes

	public static class GetDocumentUpdatesResult {

		public List<Exception> exceptionList = new ArrayList<Exception>();
		public List<DocumentStub> documentsToUpdate = new ArrayList<DocumentStub>();

	}

	// }}

	// =================================================================================
	// {{ Exceptions

	public static class DocumentActionSubmitFailedException extends Exception {

		private static final long serialVersionUID = 4380941139516219159L;

		public DocumentActionSubmitFailedException(String message) {
			super(message);
		}

		public DocumentActionSubmitFailedException(String message, Throwable throwable) {
			super(message, throwable);
		}

		public DocumentActionSubmitFailedException(Throwable throwable) {
			super(throwable);
		}

	}

	public static class DocumentUpdateFailedException extends Exception {

		private static final long serialVersionUID = 1813143116561466075L;

		public DocumentUpdateFailedException(String message) {
			super(message);
		}

		public DocumentUpdateFailedException(String message, Throwable throwable) {
			super(message, throwable);
		}

		public DocumentUpdateFailedException(Throwable throwable) {
			super(throwable);
		}

	}

	public static class DocumentDownloadFailedException extends Exception {

		private static final long serialVersionUID = 3403161712933535666L;

		public DocumentDownloadFailedException(String message) {
			super(message);
		}

		public DocumentDownloadFailedException(String message, Throwable throwable) {
			super(message, throwable);
		}

		public DocumentDownloadFailedException(Throwable throwable) {
			super(throwable);
		}

	}

	public static class DocumentSaveFailedException extends Exception {

		private static final long serialVersionUID = 2819098787313873352L;

		public DocumentSaveFailedException(String message) {
			super(message);
		}

		public DocumentSaveFailedException(String message, Throwable throwable) {
			super(message, throwable);
		}

		public DocumentSaveFailedException(Throwable throwable) {
			super(throwable);
		}

	}

	// }}

}
